//=============================================================================
//
// Controls:
// - A, D, W, S: Move Camera
// - X, Z: Zoom in, zoom out 
//
//=============================================================================

#include "MainApplication.h"

#include <cstdint>

#include "D3DErrorChecker.h"
#include "EffectManager.h"
#include "InputLayoutManager.h"
#include "LandWavesEffect.h"
#include "MathUtils.h"
#include "TextureManager.h"

namespace 
{
	const uint32_t kGridRows = 150;
	const uint32_t kGridColumns = 150;
	const uint32_t kWaveRows = 300;
	const uint32_t kWaveColumns = 300;
}

namespace D3D10
{
	MainApplication::MainApplication(const HINSTANCE hInstance)
		: D3DFramework(hInstance)
		, mLand(kGridRows, kGridColumns)
		, mWaves(kWaveRows, kWaveColumns)
		, mGrassMapRV(nullptr)
		, mSpecularMapRV(nullptr)
		, mBlendState(nullptr)		
		, mWaterTextureOffset(0.0f, 0.0f)
		, mRadius(100.0f)
		, mTheta(0.0f)
		, mPhi(D3D10Utils::PI * 0.4f)
		, mWavesTranslationOffset(0.0f)
	{
        DirectX::XMMATRIX matrix = DirectX::XMMatrixIdentity();
        DirectX::XMStoreFloat4x4A(&mLandWorld, matrix);        
        DirectX::XMStoreFloat4x4A(&mView, matrix);
        DirectX::XMStoreFloat4x4A(&mProjection, matrix);
        DirectX::XMStoreFloat4x4A(&mWVP, matrix);

        matrix = DirectX::XMMatrixTranslation(0.0f, 1.0f, 0.0f);
        DirectX::XMStoreFloat4x4A(&mWavesWorld, matrix);		
        
        mClearColor = DirectX::XMFLOAT4A(0.7f, 0.7f, 0.7f, 1.0f);
	}
	
	MainApplication::~MainApplication()
	{
		if (mDevice)
			mDevice->ClearState();
	
        D3D10Effects::EffectManager::destroyAll();
        D3D10InputLayouts::InputLayoutManager::destroyAll();
    }
	
	void MainApplication::init()
	{
		D3DFramework::init();

		mLand.init(mDevice, 1.0f, 1.0f);
		mWaves.init(mDevice, 0.5f, 0.03f, 3.25f, 0.0f);
		initLights();

        D3D10Effects::EffectManager::initAll(mDevice);
        D3D10InputLayouts::InputLayoutManager::initAll(mDevice);
        
        D3D10Utils::GetTextureManager().init(mDevice);
        mGrassMapRV = D3D10Utils::GetTextureManager().getTextureRV(L"FogEffect/grass.dds");
        mWaterMapRV = D3D10Utils::GetTextureManager().getTextureRV(L"FogEffect/water.dds");
        mSpecularMapRV = D3D10Utils::GetTextureManager().getTextureRV(L"FogEffect/defaultspec.dds");
        
		initBlendState();
		
		// Generate some waves at start up.
		for(uint32_t k = 0; k < 150; ++k)
		{ 
			uint32_t i = 5 + rand() % 250;
			uint32_t j = 5 + rand() % 250;

			const float r = D3D10Utils::RandF(0.5f, 1.25f);

			mWaves.disturb(i, j, r);
		}	
	}
	
	void MainApplication::onResize()
	{
		D3DFramework::onResize();
	
		// Update projection perspective matrix.
		const float aspect = static_cast<float> (mClientWidth) / static_cast<float> (mClientHeight);
	    const DirectX::XMMATRIX projectionMatrix = DirectX::XMMatrixPerspectiveFovLH(D3D10Utils::PI * 0.25f, aspect, 1.0f, 1000.0f);
        DirectX::XMStoreFloat4x4A(&mProjection, projectionMatrix);
    }
	
	void MainApplication::updateScene(const float deltaTime)
	{
		D3DFramework::updateScene(deltaTime);
	
		updateViewMatrix(deltaTime);

		updateWaves(deltaTime);
	}
	
	void MainApplication::drawScene()
	{
		D3DFramework::drawScene();
	
        restoreDefaultStates();

        mDevice->IASetInputLayout(D3D10InputLayouts::InputLayoutManager::mLandWavesInputLayout);
		mDevice->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

		// Set per frame constants.
        D3D10Effects::LandWavesEffect * const landWavesEffect = D3D10Effects::EffectManager::mLandWavesEffect;
        landWavesEffect->setEyePosition(mEyePosition);
        landWavesEffect->setDirectionalLight(mDirectionalLight);
        
		// Scale texture coordinates by 5 units to map [0,1]-->[0,5]
		// so that the texture repeats five times in each direction.
        DirectX::XMMATRIX aux0 = DirectX::XMMatrixScaling(5.0f, 5.0f, 1.0f);
        DirectX::XMFLOAT4X4A landTextureMatrix;
        DirectX::XMStoreFloat4x4A(&landTextureMatrix, aux0);

		// Scale and translate the texture.
        DirectX::XMMATRIX aux1 = DirectX::XMMatrixTranslation(mWaterTextureOffset.x, mWaterTextureOffset.y, 0.0f);
        aux0 = DirectX::XMMatrixMultiply(aux0, aux1);
        DirectX::XMFLOAT4X4A waterTextureMatrix;
        DirectX::XMStoreFloat4x4A(&waterTextureMatrix, aux0);
	
		D3D10_TECHNIQUE_DESC techDesc;
        landWavesEffect->mTechnique->GetDesc(&techDesc);

		ID3D10EffectPass* pass = landWavesEffect->mTechnique->GetPassByIndex(0);

		// Draw land 
        // Update Land World * View * Projection matrix
        aux0 = DirectX::XMLoadFloat4x4A(&mView);
        aux1 = DirectX::XMLoadFloat4x4A(&mProjection);
        DirectX::XMMATRIX viewProjection = XMMatrixMultiply(aux0, aux1);
        aux1 = DirectX::XMLoadFloat4x4A(&mLandWorld);
        aux0 = XMMatrixMultiply(aux1, viewProjection);
		DirectX::XMStoreFloat4x4A(&mWVP, aux0);

        landWavesEffect->setWorldViewProjectionMatrix(mWVP);
        landWavesEffect->setWorldMatrix(mLandWorld);
        landWavesEffect->setDiffuseMap(mGrassMapRV);
        landWavesEffect->setTextureMatrix(landTextureMatrix);
        landWavesEffect->setSpecularMap(mSpecularMapRV);
		pass->Apply(0);
		mLand.draw();

		// Draw waves
        // Update Waves World * View * Projection
        aux0 = DirectX::XMMatrixTranslation(0.0f, mWavesTranslationOffset, 0.0f);
        DirectX::XMStoreFloat4x4A(&mWavesWorld, aux0);
        aux0 = DirectX::XMMatrixMultiply(aux0, viewProjection);
        DirectX::XMStoreFloat4x4A(&mWVP, aux0);

        landWavesEffect->setWorldViewProjectionMatrix(mWVP);
        landWavesEffect->setWorldMatrix(mWavesWorld);
        landWavesEffect->setDiffuseMap(mWaterMapRV);
        landWavesEffect->setTextureMatrix(waterTextureMatrix);		
		landWavesEffect->setSpecularMap(mSpecularMapRV);
		pass->Apply(0);

        const float blendFactors[] = {0.0f, 0.0f, 0.0f, 0.0f};
		mDevice->OMSetBlendState(mBlendState, blendFactors, 0xffffffff);
		mWaves.draw();
		
		mSwapChain->Present(0, 0);
	}

	void MainApplication::initBlendState()
	{
		D3D10_BLEND_DESC blendDesc = {0};
		blendDesc.AlphaToCoverageEnable = false;
		blendDesc.BlendEnable[0] = true;
		blendDesc.SrcBlend = D3D10_BLEND_SRC_ALPHA;
		blendDesc.DestBlend = D3D10_BLEND_INV_SRC_ALPHA;
		blendDesc.BlendOp = D3D10_BLEND_OP_ADD;
		blendDesc.SrcBlendAlpha = D3D10_BLEND_ONE;
		blendDesc.DestBlendAlpha = D3D10_BLEND_ZERO;
		blendDesc.BlendOpAlpha = D3D10_BLEND_OP_ADD;
		blendDesc.RenderTargetWriteMask[0] = D3D10_COLOR_WRITE_ENABLE_ALL;

		HRESULT hr = mDevice->CreateBlendState(&blendDesc, &mBlendState);
		D3D10Utils::ErrorChecker(hr);
	}

	void MainApplication::initLights()
	{		
        mDirectionalLight.mAmbientColor = DirectX::XMFLOAT4A(0.4f, 0.4f, 0.4f, 1.0f);
		mDirectionalLight.mDiffuseColor = DirectX::XMFLOAT4A(1.0f, 1.0f, 1.0f, 1.0f);
		mDirectionalLight.mSpecularColor = DirectX::XMFLOAT4A(1.0f, 1.0f, 1.0f, 1.0f);	
        mDirectionalLight.mDirection = DirectX::XMFLOAT3A(0.57735f, -0.57735f, 0.57735f);
	}

    void MainApplication::restoreDefaultStates()
    {
        // Restore rasterizer state.
        mDevice->RSSetState(0);

        // Restore depth stencil state
        mDevice->OMSetDepthStencilState(0, 0);

        // Restore blend state
        const float blendFactors[] = {0.0f, 0.0f, 0.0f, 0.0f};
        mDevice->OMSetBlendState(0, blendFactors, 0xffffffff);
    }

	void MainApplication::updateViewMatrix(const float deltaTime)
	{
		// Update angles based on input to orbit camera around scene.
		if (GetAsyncKeyState('A') & 0x8000)	
			mTheta -= 1.0f * deltaTime;
		if (GetAsyncKeyState('D') & 0x8000)	
			mTheta += 1.0f * deltaTime;
		if (GetAsyncKeyState('W') & 0x8000)	
			mPhi -= 2.0f * deltaTime;
		if (GetAsyncKeyState('S') & 0x8000)	
			mPhi += 2.0f * deltaTime;
		if (GetAsyncKeyState('Z') & 0x8000)	
			mRadius -= 25.0f * deltaTime;
		if (GetAsyncKeyState('X') & 0x8000)	
			mRadius += 25.0f * deltaTime;

		// Restrict the angle mPhi and radius mRadius.
		if (mPhi < 0.1f)	
			mPhi = 0.1f;
		if (mPhi > D3D10Utils::PI - 0.1f)	
			mPhi = D3D10Utils::PI - 0.1f;

		if (mRadius < 25.0f) 
			mRadius = 25.0f;

		// Convert Spherical to Cartesian coordinates: mPhi measured from +y
		// and mTheta measured counterclockwise from -z.
		mEyePosition.x =  mRadius * sinf(mPhi) * sinf(mTheta);
		mEyePosition.z = -mRadius * sinf(mPhi) * cosf(mTheta);
		mEyePosition.y =  mRadius * cosf(mPhi);

		// Build the view matrix.
        const DirectX::XMVECTOR eyePosition = DirectX::XMLoadFloat3A(&mEyePosition);
        const DirectX::XMVECTOR target = {0.0f, 0.0f, 0.0f, 0.0f};
        const DirectX::XMVECTOR up = {0.0f, 1.0f, 0.0f, 0.0f};
        const DirectX::XMMATRIX view = DirectX::XMMatrixLookAtLH(eyePosition, target, up);
        DirectX::XMStoreFloat4x4A(&mView, view);
	}	

	void MainApplication::updateWaves(const float deltaTime)
	{
		// Animate water texture as a function of time in the update function.
		mWaterTextureOffset.y += 0.1f * deltaTime;
		mWaterTextureOffset.x = 0.25f * sinf(4.0f * mWaterTextureOffset.y);
				
		// Update waves height.
		if (GetAsyncKeyState('U') & 0x8000)	
			mWavesTranslationOffset += 5.0f * deltaTime;
		if (GetAsyncKeyState('I') & 0x8000)	
			mWavesTranslationOffset -= 5.0f * deltaTime;

		mWaves.update(deltaTime);
	}
}
